package org.unidata.mdm.meta.service.impl.measureunits.instance;

import java.util.Collection;
import java.util.Map;
import java.util.Objects;
import java.util.function.Function;
import java.util.stream.Collectors;

import javax.script.Invocable;
import javax.script.ScriptEngine;
import javax.script.ScriptEngineManager;
import javax.script.ScriptException;

import org.apache.commons.collections4.MapUtils;
import org.unidata.mdm.core.service.CustomPropertiesSupport;
import org.unidata.mdm.core.type.data.MeasuredValue;
import org.unidata.mdm.core.type.model.MeasurementCategoryElement;
import org.unidata.mdm.core.type.model.MeasurementUnitElement;
import org.unidata.mdm.core.type.model.instance.AbstractModelInstanceImpl;
import org.unidata.mdm.core.type.model.instance.AbstractNamedDisplayableCustomPropertiesImpl;
import org.unidata.mdm.core.type.model.instance.AbstractNamedDisplayableImpl;
import org.unidata.mdm.meta.configuration.TypeIds;
import org.unidata.mdm.meta.exception.MetaExceptionIds;
import org.unidata.mdm.meta.type.instance.MeasurementUnitsInstance;
import org.unidata.mdm.meta.type.model.measurement.MeasurementCategory;
import org.unidata.mdm.meta.type.model.measurement.MeasurementUnit;
import org.unidata.mdm.meta.type.model.measurement.MeasurementUnitsModel;
import org.unidata.mdm.meta.util.ModelUtils;
import org.unidata.mdm.system.exception.PlatformBusinessException;

/**
 * @author Mikhail Mikhailov on Nov 5, 2020
 */
public class MeasurementUnitsInstanceImpl
    extends AbstractModelInstanceImpl<MeasurementUnitsModel>
    implements MeasurementUnitsInstance {

    private static final String CATEGORY = "category";
    private static final String UNIT = "unit";
    private static final String VAR = "var ";
    private static final String FUNCTION = "= function (value) {return ";
    private static final String END_OF_FUNCTION = ";};";
    private static final String UNDEFINED = "= undefined;";
    private static final Double TEST_VALUE = 10d;
    private static final ScriptEngine NASHORN = new ScriptEngineManager().getEngineByName("nashorn");
    /**
     * Collected categories.
     */
    private final Map<String, MeasurementCategoryElement> categories;
    /**
     * Constructor.
     */
    public MeasurementUnitsInstanceImpl(MeasurementUnitsModel model) {
        super(model);
        this.categories = model.getValues().stream()
                .map(MeasurementCategoryImpl::new)
                .collect(Collectors.toMap(MeasurementCategoryElement::getName, Function.identity()));
    }
    /**
     * {@inheritDoc}
     */
    @Override
    public String getInstanceId() {
        return ModelUtils.DEFAULT_MODEL_INSTANCE_ID;
    }
    /**
     * {@inheritDoc}
     */
    @Override
    public String getTypeId() {
        return TypeIds.MEASUREMENT_UNITS_MODEL;
    }
    /**
     * {@inheritDoc}
     */
    @Override
    public boolean isEmpty() {
        return MapUtils.isEmpty(categories);
    }
    /**
     * {@inheritDoc}
     */
    @Override
    public Collection<MeasurementCategoryElement> getCategories() {
        return categories.values();
    }
    /**
     * {@inheritDoc}
     */
    @Override
    public MeasurementCategoryElement getCategory(String name) {
        return categories.get(name);
    }
    /**
     * {@inheritDoc}
     */
    @Override
    public boolean exists(String name) {
        return categories.containsKey(name);
    }
    /**
     * {@inheritDoc}
     */
    @Override
    public MeasurementUnitsModel toSource() {
        return new MeasurementUnitsModel()
                .withCreateDate(getCreateDate())
                .withCreatedBy(getCreatedBy())
                .withName(getName())
                .withStorageId(getStorageId())
                .withVersion(getVersion())
                .withValues(getCategories().stream()
                        .map(c -> ((MeasurementCategoryImpl) c).getSource())
                        .collect(Collectors.toList()));
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public void beforeRemove() {
        getCategories().forEach(category -> category.getUnits().forEach(unit -> ((MeasurementUnitImpl) unit).unregister()));
    }
    /**
     * @author Mikhail Mikhailov on Nov 5, 2020
     * The category.
     */
    public static class MeasurementCategoryImpl extends AbstractNamedDisplayableCustomPropertiesImpl implements MeasurementCategoryElement, CustomPropertiesSupport {
        /**
         * The units
         */
        private final Map<String, MeasurementUnitElement> units;
        /**
         * The base unit.
         */
        private MeasurementUnitElement base;
        /**
         * Constructor.
         * @param category the category.
         */
        MeasurementCategoryImpl(MeasurementCategory category) {
            super(category.getName(), category.getDisplayName(), category.getDescription(), category.getCustomProperties());
            this.units = category.getUnits().stream()
                    .map(u -> new MeasurementUnitImpl(this, u))
                    .map(u -> { if (u.isBase()) { base = u; } return u; })
                    .collect(Collectors.toMap(MeasurementUnitElement::getName, Function.identity()));
        }
        /**
         * {@inheritDoc}
         */
        @Override
        public String getId() {
            return super.getName();
        }
        /**
         * {@inheritDoc}
         */
        @Override
        public Collection<MeasurementUnitElement> getUnits() {
            return units.values();
        }
        /**
         * {@inheritDoc}
         */
        @Override
        public boolean exists(String name) {
            return units.containsKey(name);
        }
        /**
         * {@inheritDoc}
         */
        @Override
        public MeasurementUnitElement getUnit(String name) {
            return units.get(name);
        }
        /**
         * {@inheritDoc}
         */
        @Override
        public MeasurementUnitElement getBaseUnit() {
            return base;
        }
        /**
         * Gets the source view of this MUC.
         * @return source view
         */
        public MeasurementCategory getSource() {
            return new MeasurementCategory()
                    .withDescription(getDescription())
                    .withDisplayName(getDisplayName())
                    .withName(getName())
                    .withCustomProperties(assembleCustomProperties(getCustomProperties()))
                    .withUnits(getUnits().stream()
                            .map(u -> ((MeasurementUnitImpl) u).getSource())
                            .collect(Collectors.toList()));
        }
    }
    /**
     * @author Mikhail Mikhailov on Nov 5, 2020
     * The measurement unit.
     */
    public static class MeasurementUnitImpl extends AbstractNamedDisplayableImpl implements MeasurementUnitElement {
        /**
         * Is a base value?
         */
        private final boolean base;
        /**
         * Conversion function.
         */
        private final String function;
        /**
         * Compiled conversion function.
         */
        private final String functionName;
        /**
         * Constructor.
         * @param unit the unit
         */
        MeasurementUnitImpl(MeasurementCategoryImpl holder, MeasurementUnit unit) {
            super(unit.getName(), unit.getDisplayName(), unit.getDescription());
            this.base = unit.isBase();
            this.function = unit.getConversionFunction();
            this.functionName = new StringBuilder()
                    .append(CATEGORY)
                    .append("_")
                    .append(holder.getName())
                    .append("_")
                    .append(UNIT)
                    .append("_")
                    .append(this.getName())
                    .toString();

            register();
        }
        /**
         * {@inheritDoc}
         */
        @Override
        public String getId() {
            return getName();
        }
        /**
         * {@inheritDoc}
         */
        @Override
        public String getConversionFunction() {
            return function;
        }
        /**
         * {@inheritDoc}
         */
        @Override
        public boolean isBase() {
            return base;
        }
        /**
         * {@inheritDoc}
         */
        @Override
        public Double convert(Double initial) {
            Invocable invocable = (Invocable) NASHORN;
            try {
                return (Double) invocable.invokeFunction(functionName, initial);
            } catch (ScriptException | NoSuchMethodException se) {
                throw new PlatformBusinessException("Conversion function is incorrect ", se,
                        MetaExceptionIds.EX_MEASUREMENT_CONVERSION_FAILED);
            }
        }
        /**
         * {@inheritDoc}
         */
        @Override
        public void process(MeasuredValue v) {

            if (Objects.isNull(v)) {
                return;
            }

            v.setBaseValue(convert(v.getInitialValue()));
        }
        /**
         * Expected result: 'var functionName = function (value) {return value * 100;};'
         */
        public void register() {

            String jsFunction = VAR + functionName + FUNCTION + function + END_OF_FUNCTION;
            try {
                NASHORN.eval(jsFunction);
                // Test function to provoke failure
                convert(TEST_VALUE);
            } catch (ScriptException e) {
                throw new PlatformBusinessException("Cannot register conversion function: " + jsFunction, e,
                        MetaExceptionIds.EX_MEASUREMENT_CONVERSION_FAILED);
            }
        }
        /**
         * Expected result: 'var name = undefined';
         */
        public void unregister() {

            String jsFunction = VAR + functionName + UNDEFINED;
            try {
                NASHORN.eval(jsFunction);
            } catch (ScriptException e) {
                throw new PlatformBusinessException("Cannot unregister conversion function: " + jsFunction, e,
                        MetaExceptionIds.EX_MEASUREMENT_CONVERSION_FAILED);
            }
        }
        /**
         * Gets the measurement unit's source
         * @return MU source
         */
        public MeasurementUnit getSource() {
            return new MeasurementUnit()
                    .withBase(isBase())
                    .withConversionFunction(getConversionFunction())
                    .withDescription(getDescription())
                    .withDisplayName(getDisplayName())
                    .withName(getName());
        }
    }
}
